package pacman;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;

import maze.Coordinate;
import maze.Maze;
import pacman.protocol.CommandParser;
import pacman.protocol.GameProtocolHandler;
import robots.Ghost;
import robots.OwnGhost;
import robots.Pacman;

import bluetoothCommunication.Config;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;

/**
 * A class representing a game of pacman with 4 ghosts and 1 pacman
 */
public class Game {
	
	public static int NUMBEROFGHOSTS = 4;
	
	boolean VERBOSE = true;
	
	public static Coordinate[] robotStartCoordinates = { new Coordinate(0, 0), new Coordinate(0, 0), new Coordinate(0, 0), new Coordinate(0, 0) };
	private GameProtocolHandler protocolHandler;
	/**
	 * The players of this game controlling the ghosts
	 */
	private Ghost[] ghosts;
	
	/**
	 * The pacman the players will chase
	 */
	private Pacman pacman;
	
	/**
	 * Indicates the number of JOIN commands received
	 */
	public int numberOfJoins;
	
	/**
	 * The number of players that have registerd their name
	 */
	public int numberOfNamesRegistered;
	
	/**
	 * The last locations where Pacman was spotted.
	 */
	private ArrayList<Coordinate> lastPacmanLocations = new ArrayList<Coordinate>();
	
	/**
	 * The channel used for communicating
	 */
	private Channel channel;
	
	/**
	 * True if this game was started when a game was already going on.
	 */
	public boolean crashedGame;
	
	/**
	 * True if this game is filled with OwnGhosts.
	 * This disables the creating of new ghosts and the setting of names
	 */
	public boolean ownGame;
	
	public boolean stayInJoinPhase;
	
	/**
	 * Indicates wether or not this game has been fully initialised (All ghosts have been fully initialised)
	 * (True if that is the case, false if not)
	 */
	private boolean readySetGo;
	
	private QueueingConsumer consumer;
	 
	private CommandParser parser;
	
	private Timer timer;
	
	private HashMap<Ghost,Coordinate> ghostPositions;
	
	//Always make the game before making the ghosts!
	public Game() throws IOException{
		//Standaard is een game geen ownGame
		ownGame = false;
		
		this.ghosts= new Ghost[NUMBEROFGHOSTS];
		setPacman(new Pacman());
		numberOfJoins = 0;	
		numberOfNamesRegistered = 0;
		
		this.setProtocolHandler(new GameProtocolHandler(this));
		setGhostPositions(new HashMap<Ghost,Coordinate>());
		setUpRabbitMQ();

        getChannel().exchangeDeclare(Config.EXCHANGE_NAME, "fanout");
        String queueName = getChannel().queueDeclare().getQueue();
        getChannel().queueBind(queueName, Config.EXCHANGE_NAME, "");
        setConsumer(new QueueingConsumer(getChannel()));
        this.channel.basicConsume(queueName, true, getConsumer());

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        
        setParser(new CommandParser(this));
		this.setTimer(new Timer());
		
		ReceiveTask receiveCommand = new ReceiveTask();
		getTimer().schedule(receiveCommand, 10);
		stayInJoinPhase = true;
		readySetGo = false;
		crashedGame = false;
	}
	
	public Game(int numberOfGhosts) throws IOException{
		this();
		Game.NUMBEROFGHOSTS = numberOfGhosts;
		this.ghosts = new Ghost[numberOfGhosts];
	}
	
	private class ReceiveTask extends TimerTask{
		
		public ReceiveTask(){
		}
		
		@Override
		public void run() {
			while(true){
				try {
					QueueingConsumer.Delivery delivery = getConsumer().nextDelivery();
					String message = new String(delivery.getBody());
					if(VERBOSE) System.out.println("Message received: "+ message);
					getParser().parseCommand(message);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	//Zelfde staat in ghost
	private void setUpRabbitMQ() throws IOException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Config.HOST_NAME);
		Connection connection = factory.newConnection();
		setChannel(connection.createChannel());
	}
	
	/**
	 * Adds four own ghosts to this game.
	 * This will make this game an ownGame, thus disabling join/name processing
	 */
	public void addFourOwnGhost(OwnGhost [] ghosts){
		if(!(ghosts.length == NUMBEROFGHOSTS)){
			System.err.println("Lees de methodenaam nog eens aub");
		}
		for(int i = 0; i < ghosts.length; i++){
			addOwnGhost(ghosts[i]);
			ghosts[i].setSendName(true);
		}
		ownGame = true;
		stayInJoinPhase = false;
	}
	
	/**
	 * Add your own ghost to the game.
	 */
	public void addOwnGhost(OwnGhost ownGhost) {
		for (int i = 0; i < NUMBEROFGHOSTS; i++) {
			if (getGhosts()[i] == null) {
				getGhosts()[i] = ownGhost;
				addGhostPosition(ownGhost, robotStartCoordinates[i]);
				numberOfNamesRegistered++;
				return;
			}
		}
		System.err.println("Geen plaats gevonden voor de ownGhost");
	}

	/**
	 * Add a ghost from other players to this game.
	 * Will NOT send a join command.
	 * @param ghost
	 */
	public void addGhost(Ghost ghost) {
		for (int i = 0; i < NUMBEROFGHOSTS; i++) {
			if (getGhosts()[i] == null) {
				getGhosts()[i] = ghost;
				addGhostPosition(ghost, robotStartCoordinates[i]);
				if(VERBOSE) System.out.println(getOwnGhost().getName() + " voegt " + ghost.getName() + " toe aan zijn game.");
				return;
			}
		}
		System.err.println("Geen plaats gevonden voor de ghost");
	}

	public boolean isInitialized() {
		return numberOfNamesRegistered>=NUMBEROFGHOSTS;
	}
	
	public boolean stayInJoinPhase() {
		return stayInJoinPhase;
	}

	public void processCrashedRobot(String name, String version){
		if(getGhostWithName(name) == null){
			//De gecrashte ghost heeft een nieuwe naam.
			//We moeten te weten komen welke van de ghosts die nu in game zitten niet meer bestaat -> allemaal pingen en zien wie antwoord
			if(VERBOSE) System.out.println("Alle ghosts in game pingen om te weten wie er weggevallen is");
			for(OwnGhost g: getOwnGhosts()){
				g.protocolSender.sendPING("*", "LookingForCrashedGhost");
			}
			//Geef andere robots 3 seconden tijd om te antwoorden
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e){
				System.err.println("Wachten op antwoord van andere robots werd geinterrupt."); 
			}
			for(OwnGhost g: getOwnGhosts()){
				g.handleCrashedGhost(name, version);
			}
		}else{
			//Gecrashte robot gebruikt dezelfde naam als ervoor.
			Ghost crashedGhost = getGhostWithName(name);
			resetCrashedGhost(crashedGhost, name, version);
		}
	}
	
	/**
	 * Resets some attributes of a crashed ghost.
	 * 
	 * Since a crashed ghost is reset to it's starting position, some things won't be valid anymore so they need to be reset.
	 */
	public void resetCrashedGhost(Ghost crashedGhost, String name, String version){
		if(VERBOSE) System.out.println("Resetting " + crashedGhost.getName() + ", vervangen door nieuwe Ghost " + name);
		Ghost newGhost = new Ghost(new Coordinate(0, 0), this, name);
		newGhost.setVersion(version);
		replaceGhost(crashedGhost, newGhost);
		ghostPositions.remove(crashedGhost);
		ghostPositions.put(newGhost, new Coordinate(0, 0));
		if(!getOwnGhost().getMazesOtherRobots().containsKey(crashedGhost.getName())){
			System.err.println("CrashedGhost niet te vinden in otherMazes van " + getOwnGhost().getName());
		}else{
			getOwnGhost().getMazesOtherRobots().remove(crashedGhost.getName());
			Maze newMaze = new Maze(10);
			getOwnGhost().getMazesOtherRobots().put(newGhost.getName(), newMaze);
		}
		if(VERBOSE) System.out.println("Nieuwe ghost " + newGhost.getName() + " last prep laten doen.");
		newGhost.lastPreparations();
		if(VERBOSE) newGhost.printMazes();
	}

	private void replaceGhost(Ghost oldGhost, Ghost newGhost){
		for(int i = 0; i < NUMBEROFGHOSTS; i++){
			if (ghosts[i] == oldGhost){
				ghosts[i] = newGhost;
				return;
			}
		}
		System.err.println("Couldnt find the oldGhost to replace in method replaceGhost");
	}
	
	public ArrayList<Ghost> getOtherGhostsThan(String name){
		ArrayList<Ghost> list = new ArrayList<Ghost>();
		for(Ghost g: getGhosts()){
			if(g != null && !g.getName().equals(name)) list.add(g);
		}
		return list;
	}
	
	public ArrayList<OwnGhost> getOwnGhosts(){
		ArrayList<OwnGhost> list = new ArrayList<OwnGhost>();
		for(int i = 0; i < NUMBEROFGHOSTS; i++){
			if(ghosts[i] != null && ghosts[i] instanceof OwnGhost) list.add((OwnGhost) ghosts[i]);
		}
		if(list.size() == 0) System.err.println("GEEN OWNGHOSTS GEVONDEN");
		return list;
	}
	
	public ArrayList<OwnGhost> getOtherOwnGhostsThan(String name){
		ArrayList<OwnGhost> list = new ArrayList<OwnGhost>();
		for(OwnGhost g: getOwnGhosts()){
			if(!g.getName().equals(name)) list.add(g);
		}
		return list;
	}

	/**
	 * Returns the own ghost of this game.
	 * ONLY USE WHEN THIS IS NOT AN OWNGAME
	 * @return
	 */
	public OwnGhost getOwnGhost(){
		//There should only be one ownGhost
		return getOwnGhosts().get(0);
	}

	public void lastPreparations() {
		System.out.println("lastprep");
		for(Ghost ghost: getGhosts()){
			ghost.lastPreparations();
		}
		readySetGo = true;
	}
	
	/**
	 * True if this game is fully initialized.
	 */
	public boolean readySetGo(){
		return readySetGo;
	}
	
	/**
	 * Returns the player with a given name.
	 * If no player with this name is registered in this game, null is returned
	 */
	public Ghost getGhostWithName(String name){
		Ghost[] ghosts = getGhosts();
		for(int i = 0; i < NUMBEROFGHOSTS; i++){
			Ghost ghost = ghosts[i];
			if(ghost != null && ghost.getName().equals(name)) {
				return ghost;
			}
		}
		return null;
	}
	
	/**
	 * Returns the own player with a given name.
	 * If no player with this name is registered in this game, null is returned
	 */
	public OwnGhost getOwnGhostWithName(String name){
		for(OwnGhost g: getOwnGhosts()){
			if(g != null && g.getName().equals(name)) {
				return g;
			}
		}
		return null;
	}

	public ArrayList<Coordinate> getLastPacmanLocations() {
		return lastPacmanLocations;
	}

	private void setLastPacmanLocations(ArrayList<Coordinate> lastPacmanLocations) {
		this.lastPacmanLocations = lastPacmanLocations;
	}
	
	public Channel getChannel() {
		return channel;
	}

	private void setChannel(Channel channel) {
		this.channel = channel;
	}
	
	public CommandParser getParser() {
		return parser;
	}

	private void setParser(CommandParser parser) {
		this.parser = parser;
	}

	public Timer getTimer() {
		return timer;
	}

	private void setTimer(Timer timer) {
		this.timer = timer;
	}

	public Pacman getPacman() {
		return pacman;
	}

	protected void setPacman(Pacman pacman) {
		this.pacman = pacman;
	}
	
	public Ghost[] getGhosts() {
		return ghosts;
	}

	public void setGhosts(Ghost[] ghosts) {
		this.ghosts = ghosts;
		//this method is used to add the other ghosts to hashmap<String,Maze>
		//Comment out when using testFourSimulators
		//setOtherMazes(ghosts);
	}

	public QueueingConsumer getConsumer() {
		return consumer;
	}
	
	private void setConsumer(QueueingConsumer consumer) {
		this.consumer = consumer;
	}

	public HashMap<Ghost, Coordinate> getGhostPositions() {
		return ghostPositions;
	}

	public int getNumberOfJoins() {
		return numberOfJoins;
	}

	public int getNumberOfNamesRegistered() {
		return numberOfNamesRegistered;
	}

	private void setGhostPositions(HashMap<Ghost, Coordinate> ghostPositions) {
		this.ghostPositions = ghostPositions;
	}
	
	public void addGhostPosition(Ghost ghost, Coordinate co){
		if(!ghostPositions.containsKey(ghost)){
			ghostPositions.put(ghost, co);
		}else{
			System.err.println("Ghost " + ghost.getName() + " zat al in de ghostpositions.");
		}
	}

	public GameProtocolHandler getProtocolHandler() {
		return protocolHandler;
	}
	private void setProtocolHandler(GameProtocolHandler protocolHandler) {
		this.protocolHandler = protocolHandler;
	}
}